#ifndef __tenacitas_log__logger__h__
#define __tenacitas_log__logger__h__


/** @file
    In this file is defined the @p tenacitas::log::logger class
*/

/** @todo */


// ==> C++ Headers
#include <string>
#include <cstdint>
#include <mutex>
#include <map>
#include <utility>
#include <sstream>
#include <memory>
#include <functional>

// ==> 3rds Headers
#include <boost/thread/thread.hpp>
#include <boost/shared_ptr.hpp>

// ==> Our Headers
/** In the <tt>level.h</tt> file we have the definition of the log levels  */
#include "level.h"
#include <tenacitas.exception/exception.h>

// ==> Namespaces
using namespace std;

// ==> Macro Contants
/** */

// ==> Macro Commands

/** This macro allows a message to be logged in @p tenacitas::log::level::trace level

    @param  logger is an instance of some specialization of @p
    tenacitas::log::logger::ptr class */ 
#define log_trace( logger ) logger->new_line( tenacitas::log::level::trace, __FILE__, __func__, __LINE__ )

/** This macro allows a message to be logged in @p tenacitas::log::level::debug level 

    @param  logger is an instance of some specialization of @p
    tenacitas::log::logger::ptr class */ 
#define log_debug( logger ) logger->new_line( tenacitas::log::level::debug, __FILE__, __func__, __LINE__ )

/** This macro allows a message to be logged in @p tenacitas::log::level::info level 

    @param  logger is an instance of some specialization of @p
    tenacitas::log::logger::ptr class */ 
#define log_info( logger ) logger->new_line( tenacitas::log::level::info, __FILE__, __func__, __LINE__ )

/** This macro allows a message to be logged in @p tenacitas::log::level::warn level 

    @param  logger is an instance of some specialization of @p
    tenacitas::log::logger::ptr class */ 
#define log_warn( logger ) logger->new_line( tenacitas::log::level::warn, __FILE__, __func__, __LINE__ )

/** This macro allows a message to be logged in @p tenacitas::log::level::error level 

    @param  logger is an instance of some specialization of @p
    tenacitas::log::logger::ptr class */ 
#define log_error( logger ) logger->new_line( tenacitas::log::level::error, __FILE__, __func__, __LINE__ )

/** This macro allows a message to be logged in @p tenacitas::log::level::critical level 

    @param  logger is an instance of some specialization of @p
    tenacitas::log::logger::ptr class */ 
#define log_critical( logger ) logger->new_line( tenacitas::log::level::critical, __FILE__, __func__, __LINE__ )


/** ==> namespace of the organization */
namespace tenacitas {

    /** ==> namespace of the project */
    namespace log {

        // ==> Typedefs 

        // ==> Pre-Declarations

        // ==> Attributes 
        /** */

        // ==> Functions
        
        // ==> Classes

        /** Base class that provides logging facilities

            @tparam t_media offers an abstraction on the media where the
            log messages will be written. It must implement:

            <tt>typedef ptr<tt>, which defines a pointer to a @p t_media object 

            <tt>template <typename t_data>
            void write ( const t_data & )<tt>, which writes any type of data to
            the media 

            <tt>void new_line( boost::thread_id p_thread_id,
            level p_level,
            const char * p_file,
            const char * p_function,
            int64_t p_line )</tt>, which starts a new log line
            in the media, and where @p p_thread_id is the identifier of the
            thread that is starting a new log line; @p p_level is the
            tenacitas::log::level of the new line; @p p_file is the source file
            where the log is occurring; @p p_function is the function (method)
            that is log is occurring; and @p p_line is the line it the @p p_file
            where the log is  occurring

            <tt>static t_media::ptr create( boost::thread::id )</tt>, which
            creates a pointer to a @p t_media object

            You can see an example of @p t_media implementation in
            tenacitas::log::file

            <b>A word to the programmer user</b>
            You, dear programmer, should avoid using the
            tenacitas::log::logger::new_line and <tt> operator<< <tt> members,
            and use the macros commands @p log_trace, @p log_debug, @p log_info,
            @p log_warn, @p log_error or @p log_critical instead. These macros
            are defined in the beginning of this header file.

            You should use the static method tenacitas::log::logger::set_level_all
            to define the tenacitas::log::level to be used by all the @p logger
            objects.

            You can change the tenacitas::log::level for all the @p logger objects
            to be created (not the ones already created), by calling
            tenacitas::log::logger::set_level_all again.

            When creating a @p logger, you can define a tenacitas::log::level to be
            used by only for it, as you can see in the
            tenacitas::log::logger::create method below.

            You would better call tenacitas::log::logger::can_log before actualy
            logging a message to improve performance.

            @nosubgrouping
        */
        template <typename t_media>
        class logger {
        public:

            // ==> logger - Public Internal Types

            /** Type of pointer to the logger objects */
            typedef std::shared_ptr<logger> ptr;
            
            // ==> logger - Public Constructors

            /** Nothing to be done in the destructor */
            virtual ~logger( );

            /** Creates a pointer to a logger object

                @param p_level is the level specific for this logger object. It
                assumes the default value of the log level assigned to all the
                logger objects, retrieved by @p get_level_all( ) method

                @return a pointer to a @p logger object, if it was possible to
                create it, or a @p nullptr value otherwise

                @throw tenacitas::exception::exception */
            static ptr create( level p_level = get_level_all( ) );
            
            /** Copies are not-allowed */
            logger ( const logger & p_logger ) = delete;

            /** Moves are not-allowed */
            logger ( logger && p_logger ) = delete;

            // ==> logger - Public Accessors

            /** Resets the log level defined to this @p logger object

                @param  p_level is the new tenacitas::log::level to be used
                by this @p logger object. */
            void set_level( const level & p_level );

            /** @return the log level for this @p logger object */
            const level & get_level( ) const;

            /** sets the log level for all @p logger objects */
            static void set_level_all( const level & p_level );

            /** @return the log level for all @p logger objects */
            static const level & get_level_all( );


            /** Informs if a message log can be logged

                Before logging anything, one should, for performance sake, verify
                if the message will actualy be logged.
                <b>"Why?"</b>, you may ask
                Well, suppose a log such as:
                <tt> log_debug( logger ) << "very long string..."
                << a_double
                << "another long string";
                </tt>
                If the log level for the @p logger object is
                tenacitas::log::level::error, nothing will be logged. So, the work
                done to parse the parts of the log message will have been for
                nothing! Therefore, one should write:
                <tt>
                if ( logger.can_log(tenacitas::log::level::debug ) {
                log_debug( logger ) << "very long string..."
                << a_double
                << "another long string";
                }

                @param  p_level is the level one wants to check if a log
                will be logged

                @attention <b>tenacitas::log::level::error and
                tenacitas::log::level::critical will allways be logged, no matter
                the current log level. So, it is not necessary to check for the
                log level if one wants to use @p log_error or @p log_critical</b>
            */
            bool can_log( const level & p_level ) const;

            // ==> logger - Public Processors

            /** Starts a new log line

                @attention: One should not call  this method directly, but use the
                @p log_trace, @p log_debug, @p log_info, @p log_warn,
                @p logic_error or @p log_critical

                This methods inform the @p t_media class that a new line is
                starting. Depending on the concrete type of @p t_media, this can
                mean a new line in a file, a new record in a database table, or
                anything else.

                @param p_thread_id is the identifier of the thread that is starting
                a new log line
                @param  p_level is the tenacitas::log::level of the new line
                @param  p_file is the source file where the log is occurring
                @param  p_function is the function (method) that is log is
                occurring
                @param  p_line is the line it the @p p_file where the log is
                occurring

                @attention A @p logger object can not be passed to a function that
                will run in another thread. This is due to the concept that the
                tenacitas::log classes work to create log for a specific thread. So,
                if a @p logger object logs messages in a thread different from the
                one it was created, it will mix log messages for both threads.

                @throw std::string if this @p logger is being used in a
                different thread from where it was created
            */
            logger & new_line( level p_level,
                               const char * p_file,
                               const char * p_function,
                               int64_t p_line );

            // ==> logger - Public Operators
            
            /** Outputs any kind of data

                @tparam t_data is the type of the data to be logged

                @param p_data is the data to be logged
            */
            template <typename t_data>
            logger & operator << ( const t_data & p_data );
            
            /** Outputs any kind of data

                @tparam t_data is the type of the data to be logged

                @param p_data is the data to be logged
            */
            template <typename t_data>
            logger & operator << ( t_data && p_data );
            
        private:

            // ==> logger - Private Internal Types

            /** Type for the @p t_media parameter */
            typedef t_media media;

            /** Type for the pointer to a @p t_media object */
            typedef typename media::ptr media_ptr;

            /** Responsible for creatinf and mantaining @p logger objects */
            class factory {

            public:

                // ==>  factory - Public Constructors

                /** Constructor */
                factory( );

                /** Destructor */
                ~factory( );

                /** Creates ( a pointer to ) a @p logger object

                 @param p_level is the tenacitas::log::level specific to this @p
                 logger object. The default value is the tenacitas::log::level
                 defined for all the @p logger objects, retrieved by the @p
                 tenacitas::log::logger::get_level_all method

                 @return a pointer to a @p logger object, if it was possible to
                 create it, or a @p nullptr value otherwise

                 @throw tenacitas::exception::exception */
                logger::ptr create( level p_level = get_level_all( ) );


                // ==>  factory - Public Processors

                /** Retrieves a @p logger_ptr associated to a thread

                    This method will search for a @p logger_ptr in the collection of
                    @p logger_ptr, and it will return it; or it will create a
                    @p logger_ptr, associate it to the thread, and insert it into
                    the collection

                    @param p_level is the log level specific for this @p logger
                    being created

                    @param  p_thread_id is the thread one wants to retrieve the
                    @p logger_ptr associated

                */
                logger::ptr get_logger( level p_level );


                /** Deletes a <tt>(logger::ptr, boost::thread::id)</tt>
                    association from the collection 

                    @param  p_thread_id is the thread to be removed, together to
                    its associated @p logger_ptr, from the collection
                */
                void delete_logger( boost::thread::id p_thread_id );
                
            private:

                // ==>  factory - Private Internal Types

                /** Function object to be called when a thread ends */
                struct at_thread_end {

                    // ==> at_thread_end - Public Constructors

                    /** Constructor
                        @param p_this is the @p logger class itself
                    */
                    at_thread_end( factory * p_factory )
                    : m_factory ( p_factory ){}

                    // ==> at_thread_end - Public Operators

                    /** Operator to be called when a thread ends */
                    void operator( )( ) {
                        m_factory->delete_logger( boost::this_thread::get_id( ) );
                    }

                private:

                    /** @p factory object responsible for deleting a
                        <tt>(logger::ptr, boost::thread::id)</tt> association */
                    factory * m_factory;
                };

                // ==>  factory - Private Attributes

                /** Type of the collection of the thread and the media where log
                    messages will be recorded */
                typedef std::map<boost::thread::id, logger::ptr> map;

                /** The type of iterator of the collection above */
                typedef typename map::iterator iterator;

                /** collection of the thread and the media where log messages
                    will be recorded */
                map m_map;

                /** To protect access to @p m_map */
                std::mutex m_mutex;
            };


            // ==> logger - Private Constructor

            /** Constructor

                @param  p_level is the tenacitas::log::level to be used
                by this @p logger object. Its default value is the log level
                defined for all the @p logger objects, defined using the
                @p set_level_all below, and retrieved via @p get_level_all

                @note Please, read the documentation for tenacitas::log::level
                to understand how log levels work in tenacitas::log

            */
            logger( level p_level = logger::get_level_all( ) );

            // ==> logger - Private Accessors

            /** Retrieves the @p factory object */
            static factory & get_factory( );
            
            // ==> logger - Private Attributes

            /** Thread id of the thread for whom log messages are being created */
            boost::thread::id m_thread_id;

            /** Log level of this @p logger object */
            level m_level;

            /** Class that actually logs the message */
            media_ptr m_media;

            /** Log level commom to all the @p logger objects */
            static level m_level_all;
        };
    }
}

using namespace tenacitas::log;

// #############################################################################
// logger

// -----------------------------------------------------------------------------
template <typename t_media>
level
logger<t_media>::
m_level_all( level::info );

// -----------------------------------------------------------------------------
template <typename t_media>
inline logger<t_media>::
~logger( ) { }

// -----------------------------------------------------------------------------
template <typename t_media>
typename logger<t_media>::ptr
logger<t_media>::
create( level p_level ) {
    ptr l_rc;

    try {
        l_rc = get_factory( ).create( p_level );
    }
    catch ( std::exception & l_ex ) {
        tenacitas::exception::exception
            l_tenacitas_ex( make_tenacitas_exception( l_ex.what( ) ) );

        std::cerr << l_tenacitas_ex.what( ) << std::endl;
                        
        throw ( l_tenacitas_ex );
    }
    return l_rc;
}

// -----------------------------------------------------------------------------
template <typename t_media>
inline void
logger<t_media>::
set_level( const level & p_level ) {
    m_level = p_level;
}

// -----------------------------------------------------------------------------
template <typename t_media>
inline const level &
logger<t_media>::
get_level( ) const {
    return m_level;
}

// -----------------------------------------------------------------------------
template <typename t_media>
inline void
logger<t_media>::
set_level_all( const level & p_level ) {
    m_level_all = p_level;
}

// -----------------------------------------------------------------------------
template <typename t_media>
inline const level &
logger<t_media>::
get_level_all( ) {
    return m_level_all;                
}

// -----------------------------------------------------------------------------
template <typename t_media>
inline bool
logger<t_media>::
can_log( const level & p_level ) const {
    if ( ( p_level == level::error ) || ( p_level == level::critical ) ) {
        return true;
    }
    return ( p_level >= m_level );
}


// -----------------------------------------------------------------------------
template <typename t_media>
logger<t_media> &
logger<t_media>::
new_line( level p_level,
          const char * p_file,
          const char * p_function,
          int64_t p_line ) {

    logger * l_rc = nullptr;                

    try {
                    
        // check if it is still in the same thread as it was created
        if ( m_thread_id != boost::this_thread::get_id( ) ) {

            // NO! So we create a logger associated to the current
            // thread 

            ptr l_logger( get_factory( ).create( p_level ) );

            if ( l_logger ) {
                l_rc = l_logger.get( );
            }
        }
        else {
                
            // the current thread is the same as the one where this logger
            // object was created
            l_rc = this;
        }

        // but, the requested log level is allowed now?
        if ( ( l_rc ) && ( l_rc->can_log( p_level ) ) ) {
            // ok! so we ask the @p t_media object to actually
            // create a new log line
            l_rc->m_media->new_line( m_thread_id, p_level, p_file,
                                     p_function, p_line );  
        }
    }
    catch ( std::exception & l_ex ) {
        tenacitas::exception::exception
            l_tenacitas_ex( make_tenacitas_exception( l_ex.what( ) ) );

        std::cerr << l_tenacitas_ex.what( ) << std::endl;
                        
        throw ( l_tenacitas_ex );
    }
    return *l_rc;
}

// -----------------------------------------------------------------------------
template <typename t_media>
template <typename t_data>
logger<t_media> &
logger<t_media>::
operator << ( const t_data & p_data ) {
    try {
        m_media->write( p_data );
    }
    catch( std::exception & l_ex ) {
        tenacitas::exception::exception
            l_tenacitas_ex( make_tenacitas_exception( l_ex.what( ) ) );

        std::cerr << l_tenacitas_ex.what( ) << std::endl;
                        
        throw ( l_tenacitas_ex );
    }
    return *this;
}

// -----------------------------------------------------------------------------
template <typename t_media>
template <typename t_data>
logger<t_media> &
logger<t_media>::
operator << ( t_data && p_data ) {
    try {
        m_media->write( p_data );
    }
    catch( std::exception & l_ex ) {
        tenacitas::exception::exception
            l_tenacitas_ex( make_tenacitas_exception( l_ex.what( ) ) );

        std::cerr << l_tenacitas_ex.what( ) << std::endl;
                        
        throw ( l_tenacitas_ex );
    }
    return *this;
}

// -----------------------------------------------------------------------------
template <typename t_media>
inline logger<t_media>::
logger( level p_level )
    : m_thread_id( boost::this_thread::get_id( ) ),
      m_level( p_level ),
      m_media( media::create( m_thread_id ) ) {}            

// -----------------------------------------------------------------------------
template <typename t_media>
inline typename logger<t_media>::factory &
logger<t_media>::
get_factory( ) {
    static factory l_factory;
    return l_factory;
}

// #############################################################################
// logger::factory

// -----------------------------------------------------------------------------
template <typename t_media>
inline logger<t_media>::factory::
factory( )
    : m_map( ),
    m_mutex( ){}


// -----------------------------------------------------------------------------
template <typename t_media>
logger<t_media>::factory::
~factory( ){
    try {
        // first we protect access to the map
        std::lock_guard<std::mutex> lock( m_mutex );

        // for every ( logger, thread ) association...
        iterator l_end = m_map.end( );
        for ( iterator l_ite = m_map.begin( );
              l_ite != l_end;
              ++l_ite ) {

            // save a reference to the media
            logger::ptr l_logger = l_ite->second;

            // erase the association from the map
            m_map.erase( l_ite );

        }
    }
    catch ( tenacitas::exception::exception & l_ex ) {
        std::cerr << l_ex.what( ) << std::endl;
    }
    catch ( std::exception & l_ex ) {
        tenacitas::exception::exception
            l_tenacitas_ex( make_tenacitas_exception( l_ex.what( ) ) );

        std::cerr << l_tenacitas_ex.what( ) << std::endl;
    }
}

// -----------------------------------------------------------------------------
template <typename t_media>
inline typename logger<t_media>::ptr
logger<t_media>::factory::
create( level p_level ) {
    return get_logger( p_level );
}


// -----------------------------------------------------------------------------
template <typename t_media>
typename logger<t_media>::ptr
logger<t_media>::factory::
get_logger( level p_level ) {

    boost::thread::id l_thread_id =
        boost::this_thread::get_id( ); 
    try {

        // already created?
        iterator l_ite = m_map.find( l_thread_id ); 

        if ( l_ite != m_map.end( ) ) {
            // yes!
            return ( l_ite->second );
        }

        // inserting the logger into the collection of loggers,
        // associating to theirs threads
        logger::ptr l_logger ( new logger( p_level ) );

        std::pair<boost::thread::id, logger::ptr> l_pair( l_thread_id, l_logger); 

        // I can lock only here, because the thread l_th_id can not be
        // modified in another thread
        std::lock_guard<std::mutex> lock(m_mutex);

        // defining the callback to when the thread ends
        boost::this_thread::at_thread_exit( at_thread_end( this ) );

        // returning a reference to the just inserted logger
        return ( m_map.insert( l_pair ).first->second );
    }
    catch ( tenacitas::exception::exception & l_ex ) {
        std::cerr << l_ex.what( ) << std::endl;
        throw l_ex;
    }
    catch ( std::exception & l_ex ) {
        tenacitas::exception::exception
            l_tenacitas_ex( make_tenacitas_exception( l_ex.what( ) ) );

        std::cerr << l_tenacitas_ex.what( ) << std::endl;
        throw l_tenacitas_ex;
    }
}

// -----------------------------------------------------------------------------
template <typename t_media>
void
logger<t_media>::factory::
delete_logger( boost::thread::id p_thread_id ) {

    try {
        iterator l_ite = m_map.find( p_thread_id );

        if ( l_ite != m_map.end( ) ) {

            logger::ptr l_logger = l_ite->second;

            {
                // I can lock only here, because exclusion of p_thread_id can
                // not happen in another thread
                std::lock_guard<std::mutex> lock( m_mutex );

                m_map.erase( l_ite );
            }

        }
    }
    catch ( tenacitas::exception::exception & l_ex ) {
        std::cerr << l_ex.what( ) << std::endl;
        throw l_ex;
    }
    catch ( std::exception & l_ex ) {
        tenacitas::exception::exception
            l_tenacitas_ex( make_tenacitas_exception( l_ex.what( ) ) );

        std::cerr << l_tenacitas_ex.what( ) << std::endl;
        throw l_tenacitas_ex;
    }
}

#endif



